import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, simpledialog
import os
import json
import threading
import sys
from main import start_recording, playback

# Windows DPI感知支持
if sys.platform == 'win32':
    try:
        import ctypes
        # 设置DPI感知，确保在不同DPI屏幕上正确显示
        try:
            # Windows 10 1703+ 支持
            ctypes.windll.shcore.SetProcessDpiAwareness(2)  # PROCESS_PER_MONITOR_DPI_AWARE
        except (AttributeError, OSError):
            try:
                # Windows Vista+ 支持
                ctypes.windll.user32.SetProcessDPIAware()
            except (AttributeError, OSError):
                pass
    except ImportError:
        pass

recordings_dir = "recordings"
os.makedirs(recordings_dir, exist_ok=True)


class ConsoleRedirect:
    """重定向控制台输出到文本组件"""
    def __init__(self, text_widget):
        self.text_widget = text_widget
        self.stdout = sys.stdout
        self.stderr = sys.stderr
        
    def write(self, message):
        # 临时启用文本区域以插入内容
        self.text_widget.config(state=tk.NORMAL)
        self.text_widget.insert(tk.END, message)
        self.text_widget.see(tk.END)
        self.text_widget.config(state=tk.DISABLED)
        
    def flush(self):
        pass


class ScriptRecorderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("鼠标键盘录制重放工具")
        
        # 获取DPI缩放比例（用于在不同分辨率下保持相对尺寸）
        self.dpi_scale = self._get_dpi_scale()
        base_width, base_height = 700, 500
        scaled_width, scaled_height = self._scale_size(base_width, base_height)
        self.root.geometry(f"{scaled_width}x{scaled_height}")
        
        # 创建主框架
        main_frame = ttk.Frame(root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        root.columnconfigure(0, weight=1)
        root.rowconfigure(0, weight=1)
        
        # 顶部按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        self.new_script_btn = ttk.Button(button_frame, text="新建脚本", command=self.new_script)
        self.new_script_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        self.refresh_btn = ttk.Button(button_frame, text="刷新列表", command=self.refresh_script_list)
        self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        self.play_btn = ttk.Button(button_frame, text="播放脚本", command=self.play_script, state=tk.DISABLED)
        self.play_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        self.edit_btn = ttk.Button(button_frame, text="编辑信息", command=self.edit_script_info, state=tk.DISABLED)
        self.edit_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        self.delete_btn = ttk.Button(button_frame, text="删除脚本", command=self.delete_script, state=tk.DISABLED)
        self.delete_btn.pack(side=tk.LEFT)
        
        # 左侧：脚本列表
        left_frame = ttk.LabelFrame(main_frame, text="脚本列表", padding="5")
        left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
        
        # 脚本列表
        list_frame = ttk.Frame(left_frame)
        list_frame.pack(fill=tk.BOTH, expand=True)
        
        scrollbar = ttk.Scrollbar(list_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.script_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set)
        self.script_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.script_listbox.bind('<<ListboxSelect>>', self.on_script_select)
        scrollbar.config(command=self.script_listbox.yview)
        
        # 右侧：脚本详情
        right_frame = ttk.LabelFrame(main_frame, text="脚本详情", padding="5")
        right_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 详情文本区域
        self.detail_text = scrolledtext.ScrolledText(right_frame, height=15, width=50, state=tk.DISABLED)
        self.detail_text.pack(fill=tk.BOTH, expand=True)
        
        # 底部：控制台输出
        console_frame = ttk.LabelFrame(main_frame, text="控制台输出", padding="5")
        console_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
        
        self.console_text = scrolledtext.ScrolledText(console_frame, height=10, width=80, state=tk.DISABLED)
        self.console_text.pack(fill=tk.BOTH, expand=True)
        
        # 配置网格权重
        main_frame.columnconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(1, weight=1)
        main_frame.rowconfigure(2, weight=1)
        
        # 重定向控制台输出
        self.console_redirect = ConsoleRedirect(self.console_text)
        sys.stdout = self.console_redirect
        sys.stderr = self.console_redirect
        
        # 当前选中的脚本
        self.selected_script = None
        
        # 加载脚本列表
        self.refresh_script_list()
        
        # 打印欢迎信息和使用说明
        print("=" * 60)
        print("=== 连点器 - 脚本管理器 ===")
        print("=" * 60)
        print("\n使用说明：")
        print("1. 点击【新建脚本】按钮开始录制新脚本")
        print("2. 在脚本列表中选择脚本查看详情")
        print("3. 点击【播放脚本】按钮执行选中的脚本")
        print("4. 点击【编辑信息】按钮修改脚本名称和描述")
        print("5. 点击【删除脚本】按钮删除不需要的脚本")
        print("\n提示：所有操作日志都会显示在下方控制台输出区域")
        print("=" * 60 + "\n")
    
    def _get_dpi_scale(self):
        """获取DPI缩放比例，用于在不同分辨率下保持相对尺寸
        
        返回相对于96 DPI（100%缩放）的缩放比例
        例如：125%缩放返回1.25，150%缩放返回1.5
        """
        try:
            # 方法1: 使用tkinter的DPI信息（最可靠）
            # 获取1英寸对应的像素数（标准DPI是96）
            # 注意：在DPI感知模式下，这个值会反映真实的DPI
            self.root.update_idletasks()  # 确保窗口已初始化
            dpi = self.root.winfo_fpixels('1i')
            if dpi > 0:
                scale = dpi / 96.0
                return max(1.0, scale)  # 至少为1.0
        except Exception:
            pass
        
        # 方法2: 如果tkinter方法失败，尝试使用Windows API
        if sys.platform == 'win32':
            try:
                import ctypes
                # 获取系统DPI
                hdc = ctypes.windll.user32.GetDC(0)
                if hdc:
                    dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88)  # LOGPIXELSX
                    ctypes.windll.user32.ReleaseDC(0, hdc)
                    if dpi > 0:
                        scale = dpi / 96.0
                        return max(1.0, scale)
            except Exception:
                pass
        
        # 默认返回1.0（100%缩放，96 DPI）
        return 1.0
    
    def _scale_size(self, width, height):
        """根据DPI缩放比例调整尺寸
        
        在DPI感知模式下，tkinter使用真实像素值。
        为了在不同DPI屏幕上保持相对一致的显示大小（物理尺寸），
        我们需要根据DPI比例缩放窗口尺寸。
        
        例如：
        - 96 DPI (100%): 600x400 -> 600x400
        - 120 DPI (125%): 600x400 -> 750x500 (保持相同的物理大小)
        - 144 DPI (150%): 600x400 -> 900x600 (保持相同的物理大小)
        """
        return (int(width * self.dpi_scale), int(height * self.dpi_scale))
        
    def on_script_select(self, event):
        """脚本选择事件"""
        selection = self.script_listbox.curselection()
        if selection:
            index = selection[0]
            display_text = self.script_listbox.get(index)
            self.selected_script = display_text
            self.show_script_details(self.selected_script)
            self.play_btn.config(state=tk.NORMAL)
            self.edit_btn.config(state=tk.NORMAL)
            self.delete_btn.config(state=tk.NORMAL)
        else:
            self.selected_script = None
            self.detail_text.config(state=tk.NORMAL)
            self.detail_text.delete(1.0, tk.END)
            self.detail_text.config(state=tk.DISABLED)
            self.play_btn.config(state=tk.DISABLED)
            self.edit_btn.config(state=tk.DISABLED)
            self.delete_btn.config(state=tk.DISABLED)
    
    def show_script_details(self, script_name):
        """显示脚本详情"""
        file_path = os.path.join(recordings_dir, f"{script_name}.json")
        if not os.path.exists(file_path):
            return
        
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            self.detail_text.config(state=tk.NORMAL)
            self.detail_text.delete(1.0, tk.END)
            
            # 显示基本信息
            info = f"脚本名称: {script_name}\n"
            info += f"文件路径: {file_path}\n\n"
            
            # 显示描述信息
            description = data.get('description', '')
            if description:
                info += f"脚本描述: {description}\n\n"
            else:
                info += f"脚本描述: （无描述）\n\n"

            # 显示脚本运行总时间
            total_time = max(data['events'], key=lambda x: x[1])[1]
            info += f"脚本运行总时间：{total_time:.3f}s \n\n"

            
            # 显示事件信息
            if 'events' in data:
                events = data['events']
                info += f"事件总数: {len(events)}\n\n"
                info += "事件列表:\n"
                info += "-" * 60 + "\n"
                
                # 显示前20个事件
                display_count = min(20, len(events))
                for i, event in enumerate(events[:display_count]):
                    event_index = event[0] if len(event) > 0 else "?"
                    timestamp = event[1] if len(event) > 1 else "?"
                    event_type = event[2] if len(event) > 2 else "?"
                    
                    info += f"事件 #{event_index} | 时间 {timestamp:.3f}s | "
                    
                    if event_type == "click":
                        if len(event) >= 6:
                            btn = event[3]
                            rel_x = event[4]
                            rel_y = event[5]
                            btn_name = "左键" if btn == "left" else "右键" if btn == "right" else btn
                            info += f"鼠标点击 | {btn_name} | 位置 ({rel_x:.3f}, {rel_y:.3f})"
                    elif event_type == "drag":
                        if len(event) >= 5:
                            btn = event[3]
                            points = event[4] if isinstance(event[4], list) else []
                            btn_name = "左键" if btn == "left" else "右键" if btn == "right" else btn
                            info += f"鼠标拖动 | {btn_name} | {len(points)} 个路径点"
                    elif event_type == "key_press":
                        if len(event) >= 4:
                            key = event[3]
                            info += f"按键按下 | {key}"
                    elif event_type == "key_release":
                        if len(event) >= 4:
                            key = event[3]
                            info += f"按键释放 | {key}"
                    else:
                        info += f"未知类型: {event_type}"
                    
                    info += "\n"
                
                if len(events) > display_count:
                    info += f"\n... 还有 {len(events) - display_count} 个事件未显示\n"
            
            self.detail_text.insert(1.0, info)
            self.detail_text.config(state=tk.DISABLED)
            
        except Exception as e:
            self.detail_text.config(state=tk.NORMAL)
            self.detail_text.delete(1.0, tk.END)
            self.detail_text.insert(1.0, f"加载脚本详情时出错:\n{str(e)}")
            self.detail_text.config(state=tk.DISABLED)
            print(f"错误: 无法加载脚本详情 - {str(e)}")
    
    def _center_window_relative_to_parent(self, window):
        """将窗口相对于父窗口居中显示"""
        window.update_idletasks()
        # 获取父窗口位置和尺寸
        parent_x = self.root.winfo_x()
        parent_y = self.root.winfo_y()
        parent_width = self.root.winfo_width()
        parent_height = self.root.winfo_height()
        # 获取子窗口尺寸
        window_width = window.winfo_width()
        window_height = window.winfo_height()
        # 计算居中位置
        x = parent_x + (parent_width - window_width) // 2
        y = parent_y + (parent_height - window_height) // 2
        window.geometry(f"+{x}+{y}")
    
    def _create_script_info_dialog(self, title, initial_name="", initial_description="", desc_height=6):
        """创建脚本信息输入对话框（通用方法）
        
        Args:
            title: 对话框标题
            initial_name: 初始脚本名称
            initial_description: 初始脚本描述
            desc_height: 描述文本框高度
            
        Returns:
            tuple: (script_name, description, confirmed) 如果用户确认，confirmed为True
        """
        dialog = tk.Toplevel(self.root)
        dialog.title(title)
        dialog.transient(self.root)
        # 先隐藏窗口，避免闪烁
        dialog.withdraw()
        
        # 使用DPI缩放后的尺寸，确保在不同分辨率下显示相对一致
        base_width, base_height = 400, 240
        scaled_width, scaled_height = self._scale_size(base_width, base_height)
        dialog.geometry(f"{scaled_width}x{scaled_height}")
        
        result = {"script_name": None, "description": None, "confirmed": False}
        
        # 脚本名称
        ttk.Label(dialog, text="脚本名称:").pack(anchor=tk.W, padx=10, pady=(10, 5))
        name_entry = ttk.Entry(dialog, width=70)
        if initial_name:
            name_entry.insert(0, initial_name)
        name_entry.pack(padx=10, pady=(0, 10), fill=tk.X)
        
        # 脚本描述
        desc_label_text = "脚本描述（可选）:" if not initial_name else "脚本描述:"
        ttk.Label(dialog, text=desc_label_text).pack(anchor=tk.W, padx=10, pady=(0, 5))
        desc_text = scrolledtext.ScrolledText(dialog, height=desc_height, width=70)
        if initial_description:
            desc_text.insert(1.0, initial_description)
        desc_text.pack(padx=10, pady=(0, 10), fill=tk.BOTH, expand=True)
        
        # 按钮
        button_frame = ttk.Frame(dialog)
        button_frame.pack(padx=10, pady=10, fill=tk.X)
        
        def confirm():
            script_name = name_entry.get().strip()
            if not script_name:
                messagebox.showwarning("警告", "脚本名称不能为空")
                return
            
            result["script_name"] = script_name
            result["description"] = desc_text.get(1.0, tk.END).strip()
            result["confirmed"] = True
            dialog.destroy()
        
        def cancel():
            dialog.destroy()
        
        confirm_text = "确定" if not initial_name else "保存"
        ttk.Button(button_frame, text=confirm_text, command=confirm).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Button(button_frame, text="取消", command=cancel).pack(side=tk.LEFT)
        
        # 绑定回车键和ESC键
        name_entry.bind('<Return>', lambda e: confirm())
        dialog.bind('<Escape>', lambda e: cancel())
        
        # 更新窗口以确保尺寸计算正确
        dialog.update_idletasks()
        
        # 相对于主窗口居中显示（在显示前设置位置，避免闪烁）
        self._center_window_relative_to_parent(dialog)
        
        # 显示窗口并设置焦点
        dialog.deiconify()
        dialog.grab_set()
        dialog.lift()
        dialog.focus_force()
        name_entry.focus()
        
        # 等待对话框关闭
        dialog.wait_window()
        
        return result["script_name"], result["description"], result["confirmed"]
    
    def new_script(self):
        """新建脚本"""
        # 使用通用对话框方法
        script_name, description, confirmed = self._create_script_info_dialog(
            "新建脚本", 
            initial_name="", 
            initial_description="", 
        )
        
        if not confirmed:
            return
        
        # 检查是否已存在
        file_path = os.path.join(recordings_dir, f"{script_name}.json")
        if os.path.exists(file_path):
            if not messagebox.askyesno("确认覆盖", f"脚本 '{script_name}' 已存在，是否覆盖？"):
                return
        
        print(f"\n开始新建脚本: {script_name}...")
        if description:
            print(f"脚本描述: {description}")
        self.new_script_btn.config(state=tk.DISABLED)
        
        def record_thread():
            try:
                result = start_recording(name=script_name, description=description)
                if result and result[0]:
                    print(f"脚本 '{script_name}' 录制完成！\n")
                    self.root.after(0, self.refresh_script_list)
                else:
                    print("录制已取消或失败。\n")
            except Exception as e:
                print(f"录制出错: {str(e)}\n")
            finally:
                self.root.after(0, lambda: self.new_script_btn.config(state=tk.NORMAL))
        
        thread = threading.Thread(target=record_thread, daemon=True)
        thread.start()

    
    def refresh_script_list(self, preserve_selection=True):
        """刷新脚本列表"""
        # 保存当前选中的脚本名称
        selected_name = None
        if preserve_selection and self.selected_script:
            selected_name = self.selected_script
        
        # 暂时解绑事件，避免刷新时触发选择事件
        self.script_listbox.unbind('<<ListboxSelect>>')
        
        self.script_listbox.delete(0, tk.END)
        scripts = []
        if os.path.exists(recordings_dir):
            for f in os.listdir(recordings_dir):
                if f.endswith('.json'):
                    script_name = f.replace('.json', '')
                    display_text = script_name
                    scripts.append((script_name, script_name))
            
            scripts.sort(key=lambda x: x[0])  # 按名称排序
            for script_name, display_text in scripts:
                self.script_listbox.insert(tk.END, display_text)
        
        # 重新绑定事件
        self.script_listbox.bind('<<ListboxSelect>>', self.on_script_select)
        
        # 恢复选择状态
        if preserve_selection and selected_name:
            for i, (script_name, display_text) in enumerate(scripts):
                if script_name == selected_name:
                    self.script_listbox.selection_set(i)
                    self.script_listbox.see(i)
                    # 手动触发选择事件以更新详情显示
                    self.selected_script = selected_name
                    self.show_script_details(selected_name)
                    self.play_btn.config(state=tk.NORMAL)
                    self.edit_btn.config(state=tk.NORMAL)
                    self.delete_btn.config(state=tk.NORMAL)
                    break
        
        print(f"已加载 {len(scripts)} 个脚本")

    def play_script(self):
        """播放脚本"""
        if not self.selected_script:
            messagebox.showwarning("警告", "请先选择一个脚本")
            return
        
        target = self.selected_script
        # 询问播放次数
        count = simpledialog.askinteger(f"播放脚本：{target}", "请输入播放次数:", initialvalue=1, minvalue=1, maxvalue=1000)
        if count is None:
            return

        # 刷新脚本详情
        self.selected_script = target
        self.refresh_script_list()
        
        print(f"\n开始播放脚本: {target} (播放 {count} 次)")
        self.play_btn.config(state=tk.DISABLED)
        
        def play_thread():
            try:
                playback(target, count)
                print("脚本播放完成！\n")
            except Exception as e:
                print(f"播放出错: {str(e)}\n")
            finally:
                self.root.after(0, lambda: self.play_btn.config(state=tk.NORMAL))
        
        thread = threading.Thread(target=play_thread, daemon=True)
        thread.start()
    
    def edit_script_info(self):
        """编辑脚本信息"""
        if not self.selected_script:
            messagebox.showwarning("警告", "请先选择一个脚本")
            return
        
        file_path = os.path.join(recordings_dir, f"{self.selected_script}.json")
        if not os.path.exists(file_path):
            messagebox.showerror("错误", "脚本文件不存在")
            return
        
        try:
            # 读取当前脚本信息
            with open(file_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            current_description = data.get('description', '')
            
            # 使用通用对话框方法
            new_name, new_description, confirmed = self._create_script_info_dialog(
                "编辑脚本信息",
                initial_name=self.selected_script,
                initial_description=current_description,
            )
            
            if not confirmed:
                return
            
            if not new_name:
                messagebox.showwarning("警告", "脚本名称不能为空")
                return
            
            # 如果名称改变了，需要重命名文件
            if new_name != self.selected_script:
                new_file_path = os.path.join(recordings_dir, f"{new_name}.json")
                if os.path.exists(new_file_path) and new_file_path != file_path:
                    if not messagebox.askyesno("确认覆盖", f"脚本 '{new_name}' 已存在，是否覆盖？"):
                        return
            
            # 更新数据
            data['description'] = new_description
            
            # 保存文件
            try:
                if new_name != self.selected_script:
                    # 重命名文件
                    with open(new_file_path, 'w', encoding='utf-8') as f:
                        json.dump(data, f, ensure_ascii=False, indent=2)
                    if os.path.exists(file_path):
                        os.remove(file_path)
                    print(f"脚本已重命名: {self.selected_script} -> {new_name}")
                else:
                    # 只更新内容
                    with open(file_path, 'w', encoding='utf-8') as f:
                        json.dump(data, f, ensure_ascii=False, indent=2)
                
                print(f"脚本信息已更新: {new_name}")
                if new_description:
                    print(f"描述: {new_description}")
                
                self.refresh_script_list()
                
                # 如果名称改变了，清除选择
                if new_name != self.selected_script:
                    self.selected_script = None
                    self.detail_text.config(state=tk.NORMAL)
                    self.detail_text.delete(1.0, tk.END)
                    self.detail_text.config(state=tk.DISABLED)
                    self.play_btn.config(state=tk.DISABLED)
                    self.edit_btn.config(state=tk.DISABLED)
                    self.delete_btn.config(state=tk.DISABLED)
                else:
                    # 更新详情显示
                    self.show_script_details(new_name)
                
            except Exception as e:
                messagebox.showerror("错误", f"保存失败:\n{str(e)}")
                print(f"保存脚本信息出错: {str(e)}")
            
        except Exception as e:
            messagebox.showerror("错误", f"加载脚本信息失败:\n{str(e)}")
            print(f"加载脚本信息出错: {str(e)}")
    
    def delete_script(self):
        """删除脚本"""
        if not self.selected_script:
            messagebox.showwarning("警告", "请先选择一个脚本")
            return
        
        if messagebox.askyesno("确认删除", f"确定要删除脚本 '{self.selected_script}' 吗？"):
            file_path = os.path.join(recordings_dir, f"{self.selected_script}.json")
            try:
                if os.path.exists(file_path):
                    os.remove(file_path)
                    print(f"已删除脚本: {self.selected_script}")
                    self.refresh_script_list()
                    self.selected_script = None
                    self.detail_text.config(state=tk.NORMAL)
                    self.detail_text.delete(1.0, tk.END)
                    self.detail_text.config(state=tk.DISABLED)
                    self.play_btn.config(state=tk.DISABLED)
                    self.delete_btn.config(state=tk.DISABLED)
            except Exception as e:
                messagebox.showerror("错误", f"删除脚本失败:\n{str(e)}")
                print(f"删除脚本出错: {str(e)}")


def main():
    root = tk.Tk()
    app = ScriptRecorderApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()

